from re import match

class DjangoExporter:
    
    def __init__(self, db, filter=None, output=None, 
                 build_through_tables=True, build_foreign_keys=True):
        self.tables = db.tables
        # here we gather tables, that will be turned into ManyToManyFields
        self.through_tables = {}
        # here we gather tables, that were already used as ManyToMAnyFields
        self.used_through_tables = {}
        self.filter = filter
        self.output = output
        self.build_through_tables = build_through_tables
        self.build_foreign_keys = build_foreign_keys
        
    def run(self):
        if self.build_through_tables:
            self.gather_through_tables()
        self.filter_tables()
        result = ''.join([self.table2django(table) 
                          for table in self.tables.values()])
        if self.output is not None:
            with open(self.args.output, 'w') as file:
                file.write(result)
        else:
            print(result)

    def gather_through_tables(self):
        tables = {}
        for name, table in self.tables.items():
            through, explicit = self.is_through_table(table)
            if through:
                self.through_tables[name] = table, explicit
            if not through or explicit:
                tables[name] = table
        self.tables = tables

    def is_through_table(self, table):
        fks = table.foreign_keys
        is_through = (len(table.foreign_keys) == 2 and
                      fks[0].referenced_table != fks[1].referenced_table and
                      table.name != fks[0].referenced_table and
                      table.name != fks[1].referenced_table)
        return is_through, self.is_explicit_through_table(table) 
    
    def is_explicit_through_table(self, table):
        return len(table.columns) > 3
    
    def filter_tables(self):
        if self.filter is not None:
            print('# filtered using regular expression: %s' % self.filter)
            self.tables = {key: value for key, value in self.tables.items() 
                                      if match(self.filter, key)}

    def table2django(self, table):
        class_name = self.build_class_name(table)
        result = 'class %s(models.Model):\n' % class_name
        for field in self.build_fields(table):
            result += '    %s' % field
        result += """\n    class Meta:
            db_table = '%s'""" % table.name
        result += '\n\n'
        return result

    def build_class_name(self, table):
        if not isinstance(table, str):
            table = table.name
        return ''.join([part.title() for part in table.split('_')])
    
    def build_fields(self, table):
        result = []
        for column in table.columns.values():
            result.append(self.build_field(table, column))
        return result + self.build_many_to_many_fields(table)
    
    def build_field(self, table, column):
        name, type = column.name.lower(), column.type.lower()
        db_name = '' if name == column.name \
                     else ("db_column='%s'" % column.name)
        args = (name, db_name)
        if type in ('integer', 'number', 'tinyint', 'mediumint'):
            if column.name != 'id':
                # django implictly creates id field
                if self.build_foreign_keys:
                    value = self.try_foreign_key(table, column)
                else:
                    value = None
                return value if value \
                             else '%s = models.IntegerField(%s)\n' % args
        elif type in ('smallint',):
            return '%s = models.SmallIntegerField(%s)\n' % args
        elif type == 'bigint':
            return '%s = models.BigIntegerField(%s)\n' % args
        elif type in ('float', 'decimal') or type.startswith('double'):
            return '%s = models.FloatField(%s)\n' % args
        elif type == 'bool' or type == 'boolean':
            return '%s = models.BooleanField(%s)\n' % args
        elif type == 'date':
            return '%s = models.DateField(%s)\n' % args
        elif type == 'datetime' or type.startswith('timestamp'):
            return '%s = models.DateTimeField(%s)\n' % args
        elif type in ('text', 'mediumtext', 'tinytext'):
            return '%s = models.TextField(%s)\n' % args
        elif type.startswith('varchar'):
            return self.build_varchar_field(column, name, db_name)
        else:
            # can't determine type, adding warning
            return ('# failed to build field for column %s: %s\n' % 
                    (column.name, column.type))
        
    def build_varchar_field(self, column, name, db_name):
        db_name = ', ' + db_name if db_name else ''
        length = int(column.type.split('(')[1][:-1])
        return ('%s = models.CharField(max_length=%d%s)\n' % 
                (name, length, db_name))
        
    def build_many_to_many_fields(self, table):
        deleted = []
        result = []
        for through, explicit in self.through_tables.values():
            if self.needs_many_to_many(table, through):
                deleted.append(through.name)
                result.append(self.build_many_to_many_field(table, through, 
                                                            explicit))
        for through, explicit in self.used_through_tables.values():
            if self.needs_many_to_many(table, through):
                result += self.build_many_to_many_comments(table, through,
                                                           explicit)
        for name in deleted:
            self.used_through_tables[name] = self.through_tables.pop(name)
        return result
        
    def build_many_to_many_field(self, table, through_table, explicit):
        fks = through_table.foreign_keys
        index = 1 if fks[0].referenced_table == table.name else 0
        if explicit:
            args = (fks[index].referenced_table.lower(), 
                    self.build_class_name(fks[index].referenced_table),
                    self.build_class_name(through_table.name))
            return "%s = models.ManyToManyField('%s', through='%s')\n" % args            
        else:
            args = (fks[index].referenced_table.lower(), 
                    self.build_class_name(fks[index].referenced_table),
                    through_table.name)
            return "%s = models.ManyToManyField('%s', db_table='%s')\n" % args

    def build_many_to_many_comments(self, table, through_table, explicit):
        fks = through_table.foreign_keys
        index = 1 if fks[0].referenced_table == table.name else 0
        class_name = self.build_class_name(fks[index].referenced_table)
        through_name = self.build_class_name(through_table.name)
        comment = '# ManyToManyField was created in %s\n' % class_name
        if explicit:
            args = (fks[index].referenced_table.lower(), class_name,
                    through_name)
            field = "# %s = models.ManyToManyField('%s', through='%s')\n"
            return [comment, field % args]
        else:
            args = (fks[index].referenced_table.lower(), class_name,
                    through_table.name)
            field = "# %s = models.ManyToManyField('%s', db_table='%s')\n"
            return [comment, field % args]
            
    def needs_many_to_many(self, table, through_table):
        return table.name in self.referenced_tables(through_table)

    def try_foreign_key(self, table, column):
        for fk in table.foreign_keys:
            if len(fk.columns) == 1 and column.name == fk.columns[0]:
                class_name = self.build_class_name(fk.referenced_table)
                args = column.name.split('_')[0], class_name
                return "%s = models.ForeignKey('%s')\n" % args
            
    def referenced_tables(self, table):
        return tuple((fk.referenced_table for fk in table.foreign_keys))
